/*
 *  Copyright © 2018 - 2021 xulianqiang90@163.com. All Rights Reserved.
 */

package io.light.frame.dal.mybatis.generator.core.domain.mapper;

import io.light.frame.dal.mybatis.generator.core.domain.clazz.Clazz;
import io.light.frame.dal.mybatis.generator.core.domain.clazz.ClazzMethod;
import io.light.frame.dal.mybatis.generator.core.domain.clazz.ClazzMode;
import io.light.frame.dal.mybatis.generator.core.exceptions.MybatisGenException;
import io.light.frame.dal.mybatis.generator.core.util.GenToolKit;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.ibatis.annotations.Param;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.Text;
import org.springframework.beans.BeanUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

/**
 * Mapper function
 *
 * @author Ivan
 * @version 1.0.0
 * @date 2021-05-20 13:51
 */
@Getter
@Setter
@ToString
public class MapperFunc {
    public static final String ENTITY_CLAZZ_NAME_ALIAS = "object";
    private final String id;
    private final Clazz entityClazz;
    private final Type type;
    @ToString.Exclude
    private final Element element;
    private final Map<String, String> attrs;
    private final List<ClazzMethod.Param> params = new ArrayList<>();
    private boolean autoGen;
    private String parameterTypeName;
    private Clazz parameterClazz;
    private String resultTypeName;
    private Clazz resultClazz;
    private boolean returnMany;
    private String comment;
    private String content;

    public MapperFunc(Clazz entityClazz, Type type, Element element) {
        this.entityClazz = entityClazz;
        this.type = type;
        this.element = element;
        // attributes
        this.attrs = GenToolKit.elementAttributes(element);
        this.id = this.attrs.get("id");
        if (StringUtils.isBlank(id)) {
            throw new MybatisGenException(String.format("Element of function type '%s' missing attribute 'id'",
                    type));
        }
        parameterTypeName = attrs.get("parameterType");
        resultTypeName = attrs.get("resultType");
        attrs.remove("id");
        attrs.remove("parameterType");
        attrs.remove("resultType");
        try {
            this.returnMany = Boolean.parseBoolean(attrs.get("multi"));
        } catch (Exception e) {
            this.returnMany = false;
        } finally {
            attrs.remove("multi");
        }
        try {
            this.autoGen = Boolean.parseBoolean(attrs.get("autoGen"));
        } catch (Exception e) {
            this.autoGen = false;
        } finally {
            attrs.remove("autoGen");
        }
        // params
        applyParams(element);
        // result
        if (StringUtils.isBlank(resultTypeName)) {
            if (type != Type.select) {
                resultClazz = Clazz.of(int.class);
            }
        } else {
            if (ENTITY_CLAZZ_NAME_ALIAS.equalsIgnoreCase(resultTypeName) || entityClazz.getName().equals(resultTypeName)) {
                resultClazz = entityClazz;
            } else {
                resultClazz = Clazz.of(resultTypeName);
            }
        }
    }

    private void applyParams(Element element) {
        // apply parameter definition by the attribute 'parameterType'
        if (StringUtils.isNotBlank(parameterTypeName)) {
            if (ENTITY_CLAZZ_NAME_ALIAS.equalsIgnoreCase(parameterTypeName) || entityClazz.getName()
                    .equals(parameterTypeName)) {
                this.parameterClazz = entityClazz;
            } else {
                this.parameterClazz = Clazz.of(parameterTypeName);
            }
            String paramName = type == Type.select || type == Type.update ? "record" : "query";
            ClazzMethod.Param param = new ClazzMethod.Param(paramName, this.parameterClazz);
            params.add(param);
        } else {
            // apply parameter definition by the element '<parameters>...</parameters>'
            List<ClazzMethod.Param> paramList = new ArrayList<>();
            List<Node> paramMapNodeList = element.selectNodes("parameters");
            Element paramMapNode = null;
            if (paramMapNodeList != null && !paramMapNodeList.isEmpty()) {
                paramMapNode = (Element) paramMapNodeList.get(0);
            }
            if (paramMapNode != null) {
                List<Node> paramNodes = paramMapNode.selectNodes("param");
                if (paramNodes != null && !paramNodes.isEmpty()) {
                    for (Node paramNode : paramNodes) {
                        Map<String, String> paramInfo = GenToolKit.elementAttributes((Element) paramNode);
                        String paramName = paramInfo.get("name");
                        String paramType = paramInfo.get("type");
                        String paramComment = paramInfo.get("comment");
                        if (StringUtils.isBlank(paramName) || StringUtils.isBlank(paramType)) {
                            continue;
                        }
                        if (ENTITY_CLAZZ_NAME_ALIAS.equalsIgnoreCase(paramType)) {
                            paramType = entityClazz.getName();
                        }
                        boolean list;
                        try {
                            list = Boolean.parseBoolean(paramInfo.get("multi"));
                        } catch (Exception e) {
                            list = false;
                        }
                        Clazz paramClazz = entityClazz.getName().equals(paramType) ? entityClazz : Clazz.of(paramType);
                        ClazzMethod.Param param = new ClazzMethod.Param(paramName, list ?
                                Clazz.of(List.class, paramClazz) : paramClazz);
                        param.setComment(paramComment);
                        paramList.add(param);
                    }
                }
            }
            if (!paramList.isEmpty()) {
                params.addAll(paramList);
                if (params.size() == 1) {
                    parameterClazz = params.get(0).getType();
                } else {
                    params.forEach(param -> {
                        Clazz paramAnnClazz = Clazz.of("@Param(\"" + param.getName() + "\")",
                                ClazzMode.ANNOTATION);
                        param.addAnnotations(paramAnnClazz);
                        paramAnnClazz.getDeclaredClasses().add(Clazz.of(Param.class));
                    });
                }
            }
            if ((type == Type.insert || type == Type.update) && StringUtils.isBlank(parameterTypeName)
                    && params.isEmpty()) {
                parameterClazz = entityClazz;
                ClazzMethod.Param param = new ClazzMethod.Param("record", parameterClazz);
                params.add(param);
            }
        }
        //
        if (params.size() == 1) {
            ClazzMethod.Param singleParam = params.get(0);
            Class<?> singleParamClass = GenToolKit.findClass(singleParam.getType().getName());
            if (singleParamClass == null) {
                return;
            }
            if (Collection.class.isAssignableFrom(singleParamClass) || singleParamClass.isArray()
                    || BeanUtils.isSimpleValueType(singleParamClass)) {
                Clazz paramAnnClazz = Clazz.of("@Param(\"" + singleParam.getName() + "\")",
                        ClazzMode.ANNOTATION);
                paramAnnClazz.getDeclaredClasses().add(Clazz.of(Param.class));
                singleParam.addAnnotations(paramAnnClazz);
            }
        }
    }

    public void build(BiConsumer<Content, Element> appender) {
        Content content = new Content();
        if (autoGen) {
            appender.accept(content, element);
        } else {
            List<Node> contentNodes = element.content();
            // Only process the function`s first level sub nodes
            for (Node node : contentNodes) {
                String nodeName = node.getName();
                if ("parameters".equalsIgnoreCase(nodeName)) {
                    continue;
                }
                if (node instanceof Element) {
                    appender.accept(content, (Element) node);
                } else if (node instanceof Text) {
                    content.append(StringEscapeUtils.escapeXml10(node.getText()));
                } else {
                    content.append(node.asXML());
                }
            }
        }
        // Mark complete
        content.markComplete();
        appender.accept(content, element);
        this.content = content.toString().trim();
    }

    public ClazzMethod asMethod() {
        Clazz returnType = resultClazz;
        if (returnMany) {
            returnType = Clazz.of(List.class, returnType);
        }
        ClazzMethod method = new ClazzMethod(id, returnType);
        for (ClazzMethod.Param param : params) {
            method.addParam(param);
        }
        method.setComment(comment);
        return method;
    }

    /**
     * Func type
     */
    public enum Type {

        /**
         * insert
         */
        insert,
        /**
         * select
         */
        select,
        /**
         * update
         */
        update,
        /**
         * delete
         */
        delete;

        public static Type of(Element element) {
            String elementName = element.getName();
            if (StringUtils.isBlank(elementName)) {
                return null;
            }
            for (Type type : values()) {
                if (type.name().equalsIgnoreCase(elementName)) {
                    return type;
                }
            }
            return null;
        }
    }

    @Getter
    public static class Content {
        private final StringBuilder appender = new StringBuilder();
        private volatile boolean completed;

        private void markComplete() {
            this.completed = true;
        }

        public Content append(Object obj) {
            appender.append(obj);
            return this;
        }

        public Content append(String str) {
            appender.append(str);
            return this;
        }

        public int indexOf(String str) {
            return indexOf(str, 0);
        }

        public int indexOf(String str, int fromIndex) {
            return appender.indexOf(str, fromIndex);
        }

        @Override
        public String toString() {
            return appender.toString();
        }
    }
}
